(function () {
  const width = 1000, height = 1000, scale = 10

  const canvas = document.createElement('canvas')
  canvas.width = width
  canvas.height = height
  canvas.style.width = width * scale + 'px'
  canvas.style.height = height * scale + 'px'
  canvas.style.imageRendering = 'pixelated'
  const gl = canvas.getContext('webgl2')

  let framebuffer, tex, texNew, program, displayProgram

  function logCompileShader(shader, name) { // DEBUG
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.log('COMPILATION ERROR (' + name + '):\n' + gl.getShaderInfoLog(shader))
    }
  }

  function logLinkShader(program) { // DEBUG
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.log('LINKING ERROR:\n' + gl.getProgramInfoLog(program))
    }
  }

  function createShaderProgram(vertSource, fragSource) {
    const vShader = gl.createShader(gl.VERTEX_SHADER)
    const fShader = gl.createShader(gl.FRAGMENT_SHADER)

    gl.shaderSource(vShader, vertSource)
    gl.shaderSource(fShader, fragSource)
    gl.compileShader(vShader)
    logCompileShader(vShader, 'VERTEX SHADER') // DEBUG
    gl.compileShader(fShader)
    logCompileShader(fShader, 'FRAGMENT SHADER') // DEBUG

    const program = gl.createProgram()
    gl.attachShader(program, vShader)
    gl.attachShader(program, fShader)
    gl.linkProgram(program)
    logLinkShader(program) // DEBUG

    gl.deleteShader(vShader)
    gl.deleteShader(fShader)

    return program
  }

  function init() {
    framebuffer = gl.createFramebuffer()

    tex = gl.createTexture()
    gl.bindTexture(gl.TEXTURE_2D, tex)
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, width, height, 0, gl.RGB, gl.UNSIGNED_BYTE, null)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)

    texNew = gl.createTexture()
    gl.bindTexture(gl.TEXTURE_2D, texNew)
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, width, height, 0, gl.RGB, gl.UNSIGNED_BYTE, null)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)

    // vertex buffer
    const posBuffer = gl.createBuffer()
    gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer)
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      -1, -1,
       1, -1,
      -1,  1,
      -1,  1,
       1, -1,
       1,  1
    ]), gl.STATIC_DRAW)
    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0)
    gl.enableVertexAttribArray(0)

    program = createShaderProgram(vertSrc, getFragSrc(width, height))
    displayProgram = createShaderProgram(displayVertSrc, displayFragSrc)
  }

  function setPixel(x, y, value) {
    const pixel = new Uint8Array(3)
    value = !!value * 255
    for (let i = 0; i < 3; i++) {
      pixel[i] = value
    }
    gl.bindTexture(gl.TEXTURE_2D, tex)
    gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, 1, 1, gl.RGB, gl.UNSIGNED_BYTE, pixel)
  }

  function step() {
    // calculate
    gl.useProgram(program)

    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texNew, 0)
    gl.bindTexture(gl.TEXTURE_2D, tex)
    gl.uniform1i(gl.getUniformLocation(program, 'data'), 0)

    gl.drawArrays(gl.TRIANGLES, 0, 6)

    // swap textures
    const tmp = texNew
    texNew = tex
    tex = tmp
  }

  function display() {
    gl.useProgram(displayProgram)

    gl.bindFramebuffer(gl.FRAMEBUFFER, null)
    gl.bindTexture(gl.TEXTURE_2D, tex)
    gl.uniform1i(gl.getUniformLocation(displayProgram, 'data'), 0)

    gl.drawArrays(gl.TRIANGLES, 0, 6)
  }

  const btnStart = document.createElement('button')
  btnStart.innerText = 'START'
  btnStart.onclick = () => {
    if (btnStart.int == null) {
      btnStart.int = setInterval(() => {
        step()
        display()
      }, 10)
      btnStart.innerText = 'STOP'
    } else {
      clearInterval(btnStart.int)
      btnStart.int = null
      btnStart.innerText = 'START'
    }
  }
  document.body.appendChild(btnStart)

  document.body.appendChild(document.createElement('br'))

  canvas.oncontextmenu = e => e.preventDefault()
  canvas.onmouseup = e => {
    const x = Math.floor(e.offsetX / scale), y = Math.floor(e.offsetY / scale)
    if (btnStart.int != null ||
        x < 0 || x >= width ||
        y < 0 || y >= height ||
        e.button != 0 && e.button != 2) {
      return
    }
    setPixel(x, height - 1 - y, e.button == 0)
    display()
  }
  document.body.appendChild(canvas)

  init()
  display()
})()
